/** 
  A D 2.0 unit test framework inspired by Ruby's Unit::Test

// Written in the D programming language 1.0 || 2.0

Authors:	Wei Li (oldrev@gmail.com)
License:	BSD
Copyright:	Copyright (C) 2007 by Wei Li.
*/

import std.stdio;


version(Windows)
	private invariant char PATH_SEP = '\\';
else
	private invariant char PATH_SEP = '/';
////////////////////////////////////////////////////////////////////////////////

struct Failure
{
	string location;
	string message;
	string testName;
}

////////////////////////////////////////////////////////////////////////////////

struct Error
{
	Exception exception;
	string testName;
	string location;
}

////////////////////////////////////////////////////////////////////////////////

class TestResult
{
	private Error[]	m_errors;
	private Failure[] m_fails;
	private int m_runCount;
	private int m_assertionCount;
	private int m_testCount;

	const(Error)[] errors() {
		return m_errors;
	}

	const(Failure)[] failures() {
		return m_fails;
	}

	void addFailure(const string loc, const string msg, const string name)
	{
		Failure f;
		with(f) {
			location = loc;
			message = msg;
			testName = name;
		}
		m_fails ~= f;
	}

	void addError(Exception ex, const string name, const string loc) 
	{
		Error e;
		with(e) {
			exception = ex;
			testName = name;
			location = loc;
		}
		m_errors ~= e;
	}

	void addAssertion() {
		m_assertionCount++;
	}

	void addTest() {
		m_testCount++;
	}

	void addRun() {
		m_runCount++;
	}

	bool hasPassed() {
		return m_errors.length == 0 && m_fails.length == 0;
	}

	int errorCount() {
		return cast(int)m_errors.length;
	}

	int failureCount() {
		return cast(int)m_fails.length;
	}

	int runCount() {
		return m_runCount;
	}

	int testCount() {
		return m_testCount;
	}

	int assertionCount() {
		return m_assertionCount;
	}
}


////////////////////////////////////////////////////////////////////////////////


abstract class TestBase
{
	protected this() { 
	}
	
	abstract void run(TestResult result);

	abstract const bool isRunning();

	abstract const string className();

	const string fileName() 
	{
		string ret = className.idup;
		foreach(inout char c; ret)
			if(c == '.') c = PATH_SEP;
		ret ~= ".d";
		return ret;
	}

	static string removeModuleName(const string name)
	{
		size_t i = 0;
		foreach_reverse(char c; name)
		{
			if(c == '.') break;
			i++;
		}

		return name[$ - i .. $].idup;
	}

	const string name()
	{
		return removeModuleName(className);
	}

}


////////////////////////////////////////////////////////////////////////////////


abstract class TestCase(Subclass) : TestBase
{
	version(D_Version2) 
	{
		private static const(string) ctfMakeString(T)()
		{
			string ret;
			foreach(str; __traits(derivedMembers, T)) {
				if(str[0..4] == "test")
					ret ~= `registerTest("` ~ str ~ `", &sc.` ~ str ~ `); ` ~ "\n";
			}
			return ret;
		}
	}

	alias typeof(this)	SelfType;

	struct Test
	{
		string name;
		void delegate() method;
	}

	public string  m_className = Subclass.classinfo.name.idup;

	private Test[]	m_methods;

	private TestResult		m_result;
	private size_t	m_currentMethod;
	private bool	m_isFailed;
	private bool	m_running = false;


	this() {
	}

	private void initial(const Subclass sc) {
		version(D_Version2) {
			mixin(ctfMakeString!(Subclass)());
		}
	}

	const const(Test)[] tests() {
		return m_methods;
	}

	override const string className() {
		return m_className;
	}

	private void registerTest(Test t) {
		m_methods ~= t;
	}

	private void registerTest(const string name, void delegate() method) {
		registerTest(Test(name, method));
	}

	static Subclass createChild() {
		auto o = new Subclass;
		o.initial(o);
		return o;
	}

	void setup() 
	{
	}

	void teardown() 
	{
	}

	override const bool isRunning() {
		return m_running;
	}

	override void run(TestResult result) 
	{
		m_result = result;
		m_result.addRun();

		foreach(size_t i, Test t; m_methods)
		{
			m_isFailed = false;
			m_currentMethod = i;
			m_result.addTest();
			setup();
			m_running = true;

			try {
				t.method();
			}
			catch(Exception ex) {
				m_result.addError(ex, currentMethodName, fileName());
			}
			finally {
				m_running = false;
			}

			teardown();
		}
	}

	const string currentMethodName() {
		return name() ~ "." ~ m_methods[m_currentMethod].name;
	}

	private void addFailure(const string message = null)
	{
		if(!m_isFailed)
		{
			m_isFailed = true;
			m_result.addFailure(fileName(), message, currentMethodName);
		}
	}

//////////////////////////// Assertion Functions ///////////////////////////

	void assertTrue(bool x, const string message = null) 
	{
		m_result.addAssertion();
		if(!x) {
			addFailure(message);
		}
	}

	void assertNull(T)(const T value, const string message = null)
	{
		m_result.addAssertion();
		if(value !is null) {
			addFailure(message);
		}
	}

	void assertNotNull(T)(const T value, const string message = null)
	{
		m_result.addAssertion();
		if(value is null) {
			addFailure(message);
		}
	}

	void assertEqual(T)(const T expected, const T actual, const string message = null)
	{
		m_result.addAssertion();
		if(expected != actual) {
			addFailure(message);
		}
	}

	void assertNotEqual(T)(const T expected, const T actual, const T delta, const string message = null)
	{
		m_result.addAssertion();
		if(expected == actual) {
			addFailure(message);
		}
	}

	void flunk(const string message = "Flunked")
	{
		m_result.addAssertion();
		addFailure(message);
	}

}


////////////////////////////////////////////////////////////////////////////////

class TestSuit(Subclass, Tests...) : TestBase
{
	alias typeof(this)	SelfType;

	public const string m_className = Subclass.classinfo.name;
	private TestBase[]	m_tests;
	private bool m_running  = false;

	this() 
	{
		m_running = false;

		foreach(T; Tests)
		{
			T test = T.createChild();
			addTest(test);
		}
	}

	override const string className() {
		return m_className;
	}

	static Subclass createChild() {
		return new Subclass;
	}

	const(TestBase)[] tests() {
		return m_tests;
	}

	void addTest(TestBase tb)
	in {
		assert(tb !is null);
	}
	body {
		m_tests ~= tb;
	}

	const bool empty() {
		return Tests.length == 0;
	}

	override const bool isRunning() {
		return m_running;
	}

	override void run(TestResult result) {
		m_running = true;
		foreach(test; m_tests) {
			test.run(result);
		}
		m_running = false;
	}
}


static class ConsoleRunner
{
	static void showFailures(TestResult tr)
	{
		foreach(fail; tr.failures)
		{
			writefln("Failure: %s", fail.message);
			writefln("%s [%s]", fail.testName, fail.location);
			writefln();
		}
	}

	static void showErrors(TestResult tr)
	{
		foreach(err; tr.errors) 
		{
			writefln("Error: %s", err.exception.msg);
			writefln("%s [%s]", err.testName, err.location);
			writefln();
		}
	}

	static TestResult run(TestBase tb) 
	{
		auto result = new TestResult;
		writefln("Started...");
		tb.run(result);
		writefln("Finished\n");
		showErrors(result);
		showFailures(result);
		writefln();
		writefln("%d tests, %d assertions, %d failures, %d errors", 
			result.testCount, result.assertionCount, result.failureCount, result.errorCount);
		if(result.hasPassed)
			writefln("All passed.");
		return result;
	}
}

////////////////////////////////// SAMPLE //////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////


class MyTestCase : TestCase!(MyTestCase)
{
	void testOne() {
		assertTrue(false, "A stupid assertion");
		assertTrue(true);
		assertTrue(true);
		throw new Exception("Exception raised");
	}

	void testTwo() {
		assertTrue(true);
	}

	void testThree() {
		assertTrue(true);
	}
}

class MyTestCase2 : TestCase!(MyTestCase2)
{
	override void setup()
	{
	}

	override void teardown()
	{
	}

	void testOne() {
		assertTrue(true);
	}

	void testTwo() {
		assertTrue(true);
	}

	void testThree() {
		assertTrue(false, "Yet another stupid assertion");
	}
}

class MyTestCase3 : TestCase!(MyTestCase3)
{
	void testMethod() {
		assertTrue(true);
	}
}


class MyTestSuit1: TestSuit!(MyTestSuit1, MyTestCase) 
{
}

class MyTestSuit2: TestSuit!(MyTestSuit2, MyTestCase2) 
{
}

class MyTestSuit3: TestSuit!(MyTestSuit3, MyTestSuit1, MyTestSuit2, MyTestCase3)
{
}

void main()
{
	auto ts = new MyTestSuit3;

	ConsoleRunner.run(ts);
}
